/*
 * Ani
 * Copyright (C) 2022-2024 Him188
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package me.him188.ani.datasources.api.source

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import me.him188.ani.datasources.api.CachedMedia
import me.him188.ani.datasources.api.DefaultMedia
import me.him188.ani.datasources.api.Media
import me.him188.ani.datasources.api.paging.SizedSource
import me.him188.ani.datasources.api.topic.EpisodeRange
import me.him188.ani.datasources.api.topic.ResourceLocation

/**
 * 一个查询单个剧集的可下载的资源 [Media] 的服务, 称为数据源 [MediaSource].
 *
 * 数据源不提供条目数据, 而是依赖条目服务 (即 Bangumi) 提供的条目数据.
 * 数据源的查询 [fetch] 可以拿到包含条目信息的 [MediaFetchRequest].
 * 数据源只需要支持使用 [MediaFetchRequest] 中的信息, 查询该剧集的所有可下载资源 [Media].
 *
 * [MediaSource] 是一个抽象的来源. 它不一定都是来自网络和 BT, 也可以是本地文件系统.
 * 用户使用缓存功能创建的缓存, 就会存储到缓存管理器 `MediaCacheManager`, 然后能通过一个专门查询本地缓存的 [MediaSource] 查询到.
 *
 * ## [MediaSource] 只负责查询资源 ([Media]) 列表
 *
 * 对于资源的下载, 缓存, 以及播放, 都是由其他模块负责. 具体内容可查看:
 * - 下载过程: `MediaCacheEngine`
 * - 管理缓存列表: `MediaCacheManager`
 * - 解析 [Media] 为可播放的视频数据: `VideoSourceResolver`
 *
 * ## 资源信息
 *
 * 数据源查询到的资源, 为 [Media]. 详细查看 [Media]. 对于在线数据源, 通常为 [DefaultMedia].
 * [CachedMedia] 只有缓存数据源才会返回.
 *
 * ## 数据源全局唯一
 *
 * 每个数据源都拥有全局唯一的 ID [mediaSourceId], 可用于保存用户偏好, 识别缓存资源的来源等.
 *
 * ## 加载和配置数据源
 *
 * [MediaSource] 实际上需要通过工厂 [MediaSourceFactory.create] 构造.
 *
 * [MediaSourceFactory] 为数据源定义了可配置参数 [MediaSourceFactory.parameters], 并能使用这些参数[创建][MediaSourceFactory.create]一个示例.
 *
 * 详细查看 [MediaSourceFactory].
 *
 * ### 使 APP 能够检测到新的 [MediaSource] 的示例步骤
 *
 * 假设你已经实现了一个数据源, 名为 `foo`, 模块位置为 `:data-sources:foo`.
 * 1. 在 `data-sources/foo/resources/META-INF/services` 目录下创建一个名为 `me.him188.ani.datasources.api.source.MediaSourceFactory` 的文件
 * 2. 在文件中写入你的 `MediaSourceFactory` 的全限定类名, 例如 `me.him188.ani.datasources.api.source.impl.MyMediaSourceFactory`
 * 3. 在 `MyMediaSourceFactory` 中实现 `create` 方法, 根据传入的 [MediaSourceConfig], 构造并返回你的 [MediaSource] 实例
 * 4. 在 `:app:shared` 中的 `build.gradle.kts` 搜索 `api(projects.dataSources.core)`, 找到现有数据源的依赖定义,
 * 仿照着增加一行你的模块: `api(projects.dataSources.foo)`
 * 5. 现在启动 app 便可以自动加载你的数据源了, 可在设置中验证
 *
 * @see MediaSourceFactory
 */
interface MediaSource : AutoCloseable {
    /**
     * 全局唯一的 ID. 可用于保存用户偏好, 识别缓存资源的来源等.
     */
    val mediaSourceId: String

    /**
     * 数据源 [MediaSource] 以及资源 [Media] 的存放位置,
     * 因为一个资源既可以来源于网络, 也可以来自本地文件系统等.
     */
    val location: MediaSourceLocation
        get() = MediaSourceLocation.Online

    /**
     * 数据源类型. 不同类型的资源在缓冲速度上可能有本质上的区别.
     */
    val kind: MediaSourceKind

    /**
     * 检查该数据源是否可用.
     *
     * @see Boolean.toConnectionStatus
     */ // 这会在设置的数据源测试中使用.
    suspend fun checkConnection(): ConnectionStatus

    /**
     * 使用 [MediaFetchRequest] 中的信息, 尽可能多地查询一个剧集的所有可下载的资源, 返回一个分页的资源列表.
     *
     * 数据源应当尽可能*精准*地返回结果:
     * - 对于**完全**肯定匹配的资源, 标记为 [MatchKind.EXACT].
     * - 对于**完全**肯定不不配的资源, 需要剔除.
     * - 对于无法 100% 区分的, 则应当返回, 并标记为 [MatchKind.FUZZY].
     *
     * ## 数据源选择的实现细节
     *
     * ### 数据源需要负责区分剧集的正确性
     * 若请求 [MediaFetchRequest.episodeSort] 为 "01", 但 [fetch] 返回 "02", 该剧集**不会**被后续流程自动剔除, 它会被原封不动地展示给用户.
     *
     * 所有 [fetch] 返回的资源, 都将会被数据源选择器 `MediaSelector` 接收并能够显示.
     * 但需要注意数据源选择系统有一系列过滤选项 (APP 设置中 "播放与缓存" 的 "高级设置")
     *
     * 当用户关闭设置中的所有自动过滤选项时, 将能够看到所有 [fetch] 返回的资源.
     *
     * ###
     */
    suspend fun fetch(query: MediaFetchRequest): SizedSource<MediaMatch>

    override fun close() {}
}

/**
 * 数据源类型.
 *
 * 不同类型的资源在缓冲速度上可能有本质上的区别.
 */ // 在数据源选择器中, 每个数据源会以 [MediaSourceKind] 分类展示.
@Serializable
enum class MediaSourceKind {
    /**
     * 在线视频网站. 资源为 [ResourceLocation.WebVideo] 或 [ResourceLocation.HttpStreamingFile].
     *
     * 对于完结番, [WEB] 的单集资源**不会**被过滤掉.
     * @see BitTorrent
     */
    WEB,

    /**
     * P2P BitTorrent 网络. 资源为 [ResourceLocation.HttpTorrentFile] 或 [ResourceLocation.MagnetLink].
     *
     * 如果 [Media.episodeRange] 剧集为单集 [EpisodeRange.single], 且类型为 [MediaSourceKind.BitTorrent], 则很有可能会因为默认启用的"完结番隐藏单集资源"功能而被过滤掉.
     * @see WEB
     * @see Media.episodeRange
     */
    BitTorrent,

    /**
     * 本地视频缓存. 只表示那些通过 `MediaCacheManager` 缓存的视频.
     *
     * 该类型的资源总是会显示, 忽略一切过滤条件.
     */
    LocalCache;

    companion object {
        /**
         * 除本地缓存这种特殊类型外, 用户可以选择的数据源类型.
         */
        val selectableEntries = listOf(WEB, BitTorrent)
    }
}


/**
 * 数据源 [MediaSource] 以及资源 [Media] 的存放位置.
 */
@Serializable(MediaSourceLocation.AsStringSerializer::class)
sealed class MediaSourceLocation {
    /**
     * 资源位于公共网络. 例如一个在线视频网站, 或者 BitTorrent 网络.
     */
    data object Online : MediaSourceLocation()

    /**
     * 资源位于当前局域网内 (下载很快延迟很低). 例如 NAS 或自建的视频服务器.
     * 如果需要通过公网访问自建视频服务器, 那该服务器属于 [Online] 而不是 [Lan].
     */
    data object Lan : MediaSourceLocation()

    /**
     * 资源位于本地文件系统. 必须是能通过 `File` 直接访问的.
     */
    data object Local : MediaSourceLocation()

    companion object {
        val entries = listOf(Online, Lan, Local)
    }

    internal object AsStringSerializer : KSerializer<MediaSourceLocation> {
        override val descriptor = String.serializer().descriptor

        override fun serialize(encoder: Encoder, value: MediaSourceLocation) {
            encoder.encodeString(
                getText(value),
            )
        }

        private fun getText(value: MediaSourceLocation) = when (value) {
            Online -> "ONLINE"
            Lan -> "LAN"
            Local -> "LOCAL"
        }

        override fun deserialize(decoder: Decoder): MediaSourceLocation {
            val string = decoder.decodeString()
            for (entry in entries) { // type safe
                if (getText(entry) == string) {
                    return entry
                }
            }
            throw SerializationException("Unknown MediaSourceLocation: $string")
        }
    }
}

fun MediaSourceLocation.isLowEffort(): Boolean = this is MediaSourceLocation.Lan || this is MediaSourceLocation.Local
